package je3.sound;

import javax.sound.midi.*;
import javax.sound.sampled.*;
import java.io.IOException;
import java.net.URL;

/**
 * This class plays sounds streaming from a URL: it does not have to preload
 * the entire sound into memory before playing it. It is a command-line
 * application with no gui. It includes code to convert ULAW and ALAW
 * audio formats to PCM so they can be played. Use the -m command-line option
 * before MIDI files.
 */
public class PlaySoundStream {
    // Create a URL from the command-line argument and pass it to the
    // right static method depending on the presence of the -m (MIDI) option.
    public static void main(String[] args) throws Exception {
        if (args[0].equals("-m")) streamMidiSequence(new URL(args[1]));
        else streamSampledAudio(new URL(args[0]));

        // Exit explicitly.
        // This is needed because the audio system starts background threads.
        System.exit(0);
    }

    /**
     * Read sampled audio data from the specified URL and play it
     */
    public static void streamSampledAudio(URL url)
            throws IOException, UnsupportedAudioFileException,
            LineUnavailableException {
        AudioInputStream ain = null;  // We read audio data from here
        SourceDataLine line = null;   // And write it here.

        try {
            // Get an audio input stream from the URL
            ain = AudioSystem.getAudioInputStream(url);

            // Get information about the format of the stream
            AudioFormat format = ain.getFormat();
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);

            // If the format is not supported directly (i.e. if it is not PCM
            // encoded), then try to transcode it to PCM.
            if (!AudioSystem.isLineSupported(info)) {
                // This is the PCM format we want to transcode to.
                // The parameters here are audio format details that you
                // shouldn't need to understand for casual use.
                AudioFormat pcm =
                        new AudioFormat(format.getSampleRate(), 16,
                                format.getChannels(), true, false);

                // Get a wrapper stream around the input stream that does the
                // transcoding for us.
                ain = AudioSystem.getAudioInputStream(pcm, ain);

                // Update the format and info variables for the transcoded data
                format = ain.getFormat();
                info = new DataLine.Info(SourceDataLine.class, format);
            }

            // Open the line through which we'll play the streaming audio.
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(format);

            // Allocate a buffer for reading from the input stream and writing
            // to the line.  Make it large enough to hold 4k audio frames.
            // Note that the SourceDataLine also has its own internal buffer.
            int framesize = format.getFrameSize();
            byte[] buffer = new byte[4 * 1024 * framesize]; // the buffer
            int numbytes = 0;                               // how many bytes

            // We haven't started the line yet.
            boolean started = false;

            for (; ;) {  // We'll exit the loop when we reach the end of stream
                // First, read some bytes from the input stream.
                int bytesread = ain.read(buffer, numbytes, buffer.length - numbytes);
                // If there were no more bytes to read, we're done.
                if (bytesread == -1) break;
                numbytes += bytesread;

                // Now that we've got some audio data to write to the line,
                // start the line, so it will play that data as we write it.
                if (!started) {
                    line.start();
                    started = true;
                }

                // We must write bytes to the line in an integer multiple of
                // the framesize.  So figure out how many bytes we'll write.
                int bytestowrite = (numbytes / framesize) * framesize;

                // Now write the bytes. The line will buffer them and play
                // them. This call will block until all bytes are written.
                line.write(buffer, 0, bytestowrite);

                // If we didn't have an integer multiple of the frame size,
                // then copy the remaining bytes to the start of the buffer.
                int remaining = numbytes - bytestowrite;
                if (remaining > 0)
                    System.arraycopy(buffer, bytestowrite, buffer, 0, remaining);
                numbytes = remaining;
            }

            // Now block until all buffered sound finishes playing.
            line.drain();
        }
        finally { // Always relinquish the resources we use
            if (line != null) line.close();
            if (ain != null) ain.close();
        }
    }

    // A MIDI protocol constant that isn't defined by javax.sound.midi
    public static final int END_OF_TRACK = 47;

    /* MIDI or RMF data from the specified URL and play it */
    public static void streamMidiSequence(URL url)
            throws IOException, InvalidMidiDataException, MidiUnavailableException {
        Sequencer sequencer = null;     // Converts a Sequence to MIDI events
        Synthesizer synthesizer = null; // Plays notes in response to MIDI events

        try {
            // Create, open, and connect a Sequencer and Synthesizer
            // They are closed in the finally block at the end of this method.
            sequencer = MidiSystem.getSequencer();
            sequencer.open();
            synthesizer = MidiSystem.getSynthesizer();
            synthesizer.open();
            sequencer.getTransmitter().setReceiver(synthesizer.getReceiver());

            // Specify the InputStream to stream the sequence from
            sequencer.setSequence(url.openStream());

            // This is an arbitrary object used with wait and notify to
            // prevent the method from returning before the music finishes
            final Object lock = new Object();

            // Register a listener to make the method exit when the stream is
            // done. See Object.wait( ) and Object.notify( )
            sequencer.addMetaEventListener(new MetaEventListener() {
                public void meta(MetaMessage e) {
                    if (e.getType() == END_OF_TRACK) {
                        synchronized (lock) {
                            lock.notify();
                        }
                    }
                }
            });

            // Start playing the music
            sequencer.start();

            // Now block until the listener above notifies us that we're done.
            synchronized (lock) {
                while (sequencer.isRunning()) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
        }
        finally {
            // Always relinquish the sequencer, so others can use it.
            if (sequencer != null) sequencer.close();
            if (synthesizer != null) synthesizer.close();
        }
    }
}
